package nl.utwente.ewi.hmi.multitouch.analysis;

import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javolution.util.FastMap;
import nl.utwente.ewi.hmi.multitouch.Tangible;

public class SideEffectFilter implements HypothesisFilter {

	private Set<Segment> sideEffects = null;

	private Set<Segment> canBeSideEffects = null;
	private Set<Segment> isNotSideEffects = null;
		
	public SideEffectFilter() {
		this.sideEffects = new HashSet<Segment>();

		this.canBeSideEffects = new HashSet<Segment>();
		this.isNotSideEffects = new HashSet<Segment>();
	}
	
	/**
	 * Remove all hypotheses which are part of a side effect
	 */
	@Override
	public Set<Hypothesis> filter(Set<Hypothesis> hypotheses) {
		this.canBeSideEffects.clear();
		this.isNotSideEffects.clear();
		
		// Update side effects with segments which have inheriting properties
		for(Hypothesis hypothesis : hypotheses) {
			Segment fromSegment = hypothesis.getFromSegment();
			Segment toSegment = hypothesis.getToSegment();

			if(toSegment == null) {
				continue;
			}

			if(isOldSideEffect(fromSegment)) { // A side effect is tracked to a target segment, the target segment is a side effect candidate
				canBeSideEffect(toSegment);
			} else if(fromSegment != null){ 
				// A non side effect is tracked to a segment, the target segment is not a side effect
				isNotSideEffect(toSegment);
			}
		}

		// Collect new side effects
		collectNewSideEffects(hypotheses);
		
		// Remove all hypotheses which reveal any side effects
		Iterator<Hypothesis> hypothesesIterator = hypotheses.iterator();
		
		while(hypothesesIterator.hasNext()) {
			Hypothesis hypothesis = hypothesesIterator.next();

			// If a side effect occurs by inheriting
			if(isNewSideEffect(hypothesis.getToSegment())) {
				hypothesesIterator.remove();
			// Or if a side effects is tracked to a non side effect element (side effect released or on join)
			} else if(isOldSideEffect(hypothesis.getFromSegment())) {
				hypothesesIterator.remove();
			}
			
		}

		// Swap sets
		Set<Segment> temp = this.sideEffects;
		this.sideEffects = canBeSideEffects;
		this.canBeSideEffects = temp;

		return hypotheses;
	}

	/**
	 * 
	 * @param segment
	 * @return True if the segment was previously a side effect
	 */
	private boolean isOldSideEffect(Segment segment) {
		return this.sideEffects.contains(segment);
	}

	/**
	 * 
	 * @param segment
	 * @return True if the segment is a newly registered side effect
	 */
	private boolean isNewSideEffect(Segment segment) {
		return this.canBeSideEffects.contains(segment);
	}

	/**
	 * Denote the segment as a possible side effect, if
	 * the segment is already denoted as not a side effect,
	 * the segment remains denoted as not a side effect.
	 * 
	 * @param segment
	 */
	private void canBeSideEffect(Segment segment) {
		if(isNotSideEffects.contains(segment)) {
			return;
		}
		this.canBeSideEffects.add(segment);
	}

	/**
	 * Denote the segment as not a side effect.
	 * 
	 * @param segment
	 */
	private void isNotSideEffect(Segment segment) {
		this.canBeSideEffects.remove(segment);
		this.isNotSideEffects.add(segment);
	}

	/**
	 * Collect new side effects
	 * 
	 * @param hypotheses
	 */
	private void collectNewSideEffects(Set<Hypothesis> hypotheses) {
		FastMap<Tangible, Set<Range>> xRangesMap = FastMap.newInstance();
		FastMap<Tangible, Set<Range>> yRangesMap = FastMap.newInstance();

		// Add all horizontal and vertical ranges
		for(Hypothesis hypothesis : hypotheses) {
			Segment segment = hypothesis.getToSegment();

			// A dead end hypothesis doesn't denote a tracked segment
			if(segment == null) {
				continue;
			}
			Rectangle2D bounds = segment.getBounds();
			
			if(!xRangesMap.containsKey(segment.getTangible())) {
				xRangesMap.put(segment.getTangible(), new HashSet<Range>());
			}
			
			Set<Range> xRanges =  xRangesMap.get(segment.getTangible());
			
			if(!yRangesMap.containsKey(segment.getTangible())) {
				yRangesMap.put(segment.getTangible(), new HashSet<Range>());
			}
			Set<Range> yRanges =  yRangesMap.get(segment.getTangible());

			xRanges.add(new Range(bounds.getMinX(), bounds.getMaxX()));
			yRanges.add(new Range(bounds.getMinY(), bounds.getMaxY()));
		}

		
		// Remove all known horizontal and vertical ranges
		for(Segment segment : this.isNotSideEffects) {
			Rectangle2D bounds = segment.getBounds();

			if(!xRangesMap.containsKey(segment.getTangible())) {
				xRangesMap.put(segment.getTangible(), new HashSet<Range>());
			}
			
			Set<Range> xRanges =  xRangesMap.get(segment.getTangible());
			
			if(!yRangesMap.containsKey(segment.getTangible())) {
				yRangesMap.put(segment.getTangible(), new HashSet<Range>());
			}
			Set<Range> yRanges =  yRangesMap.get(segment.getTangible());
			
			xRanges.remove(new Range(bounds.getMinX(), bounds.getMaxX()));
			yRanges.remove(new Range(bounds.getMinY(), bounds.getMaxY()));
		}
		
		// For each group of tangibles, identify the side effects
		for(Tangible tangible : xRangesMap.keySet()) {
			Set<Range> xRanges = xRangesMap.get(tangible);
			Set<Range> yRanges = yRangesMap.get(tangible);

			// If either set of ranges is empty, nothing can be said about side effects
			if(xRanges.isEmpty() || yRanges.isEmpty()) {
				return;
			}
	
			// TODO : Worst case complexity is now O(n^2), could (and should) be n
			// Else, all new elements in the x and y range which are not in both ranges are side effects
			for(Hypothesis hypothesis : hypotheses) {
				Segment segment = hypothesis.getToSegment();
				
				// only push new segments as possible side effects (trust the tracking algorithm)
				if(hypothesis.getFromSegment() != null) {
					continue;
				}
				
				// End of trajectory can not denote a side effect, different tangible does not correspond
				if(segment == null || segment.getTangible() != tangible) {
					continue;
				}
	
				Rectangle2D bounds = segment.getBounds();
	
				boolean inY = yRanges.contains(new Range(bounds.getMinY(), bounds.getMaxY()));
				boolean inX = xRanges.contains(new Range(bounds.getMinX(), bounds.getMaxX()));
	
				// (inY || inX) && !(inY && inX)
				if(inY ^ inX) {
					canBeSideEffect(segment);
				} else {
					isNotSideEffect(segment);
				}
			}
		}
		FastMap.recycle(yRangesMap);
		FastMap.recycle(xRangesMap);
	}

	private static class Range {
		final long from;
		final long to;
		
		Range(double from, double to) {
			this.from = Double.doubleToLongBits(from);
			this.to = Double.doubleToLongBits(to);
		}

		@Override
		public int hashCode() {
			return (int) ((from ^ (from >>> 32)) ^ (to ^ (to >>> 32)));
		}

		@Override
		public boolean equals(Object obj) {
			if(obj == null || !(obj instanceof Range)) {
				return false;
			}

			Range other = (Range) obj;
			return other.from == this.from && other.to == this.to;
		}
	}
}
